import { useQueryValue } from '@/components';
import { get, invert } from '@/utils/fp';

export function isObject(object) {
  return toString.call(object) === '[object Object]';
}

/**
 * 枚举值对象转为options参数
 * @param {Object} optionsMap
 * @return {{label: *, value: *}[]}
 */
export function enumToOptions(optionsMap) {
  return Object.keys(optionsMap).map((v) => ({ value: v === 'null' ? null : v, label: optionsMap[v] }));
}

function to(textOrFn, object) {
  return typeof textOrFn === 'function' ? textOrFn(object) : object[textOrFn];
}

/**
 * options转为枚举对象
 * @param {Array<{label: string, value: string | number}>} options
 * @param {String|function(object):String|Number} labelKey
 * @param {String|function(object):String|Number} valueKey
 * @return {{ [value]: label }}
 */
export function optionsToMap(options, labelKey = 'label', valueKey = 'value') {
  return options.reduce((ret, o) => ({ ...ret, [to(valueKey, o)]: to(labelKey, o) }), {});
}

/**
 * 字典列表转为枚举对象
 * @param {DictionaryItem[]} dictList
 * @return {{ [code]: cnName }}
 */
export function dictToMap(dictList) {
  return dictList.reduce(
    (ret, d) => ({
      ...ret,
      [d.code]: d.cnName,
    }),
    {},
  );
}

/**
 * 获取URL中的参数字符串，包含哈希路径中的参数和location中的参数
 * @param url {String} URL字符串
 * @return {String[]}
 */
export function splitUrlQuery(url) {
  return url.split('#').reduce((a, v) => {
    const query = v.split('?')[1];
    if (!query) return a;
    return [...a, query];
  }, []);
}

/**
 * 将URL参数字符串解析为对象
 * @param queryString {string} URL参数形式的字符串
 * @return {Object} 参数对象
 */
export function parseQueryString(queryString) {
  return queryString.split(/&/g).reduce((a, kv) => {
    const [key, val = ''] = kv.split('=');
    if (!key) return a;
    const value = val === '' ? true : decodeURIComponent(val);
    return { ...a, [key]: value };
  }, {});
}

/**
 * 获取url中的参数，query中和hash中的参数都支持
 * @param key
 * @return {String|boolean}
 */
export function getSearchString(key) {
  const queryString = splitUrlQuery(window.location.href).join('&');
  return parseQueryString(queryString)[key];
}

/**
 * 从提供search中获取参数
 * @param {String} search
 * @param {String} key
 * @return {string}
 */
export function getQueryValue(search, key) {
  const params = new URLSearchParams(search);
  return params.get(key);
}

export function getQueryValues(search, ...keys) {
  const params = new URLSearchParams(search);
  return keys.map(params.get, params);
}

/**
 * 对象转URL参数字符串
 * @param object
 * @return {string} URL参数字符串
 */
export function objectToParamStr(object) {
  return Object.keys(object)
    .map((key) => {
      const val = object[key];
      if (['function', 'undefined'].includes(val) || val === null) return false;
      return `${key}=${encodeURIComponent(typeof val === 'object' ? JSON.stringify(val) : val)}`;
    })
    .filter(Boolean)
    .join('&');
}

/**
 * 当前调用栈结束后执行
 * 事件循环机制
 * @param {Function} fn
 * @param {any[]} args
 */
export function nextTick(fn, ...args) {
  return new Promise((res, rej) => {
    require('next-tick')(function () {
      try {
        res(fn?.apply(this, args));
      } catch (e) {
        rej(e);
      }
    });
  });
}

/**
 * 传入一个函数，返回一个函数，用法与loadsh.throttle/debounce类似
 * 返回的函数只会在当前调用栈结束后执行
 * 返回的函数如果在一个事件周期内被多次调用，只会被调用一次
 * 异步返回执行结果
 * @param {Function} fn
 * @return {function(): Promise<any>}
 */
export function createNextTickOnce(fn) {
  let done;
  let result;
  return () => {
    done = false;
    result = null;
    return new Promise((resolve) => {
      nextTick(() => {
        if (!done) {
          result = fn?.();
          done = true;
        }
        resolve(result);
      });
    });
  };
}

export function createCurrentOnce(fn) {
  let done;
  let result;
  return () => {
    nextTick(() => {
      done = false;
      result = null;
    });
    if (!done) {
      result = fn?.();
      done = true;
    }
    return result;
  };
}

/**
 * 下一个事件循环
 * @return {Promise<void>}
 */
export function nextLoop(value) {
  return new Promise((res) => {
    setTimeout(res, 0, value);
  });
}

/**
 * 等待
 * @param {number} time
 * @return {Promise<void>}
 */
export function wait(time = 0) {
  return new Promise((res) => {
    setTimeout(res, time);
  });
}

export function pathJoin(...paths) {
  let pathString = paths
    .filter((s) => typeof s === 'string')
    .join('/')
    .replace(/[/]{2,}/g, '/')
    .split('/')
    .reduce((ret, path) => (path === '..' ? ret.slice(0, -1) : [...ret, path]), [])
    .join('/');
  const [last] = paths.slice(-1);
  if (typeof last === 'object' && last !== null) {
    pathString += `?${objectToParamStr(last)}`;
  }
  return pathString;
}

export function pathJoinParams(...pathsOrParams) {
  let origin = '';
  let [first] = pathsOrParams;
  if (typeof first === 'string' && /^https?:\/\//i.test(first)) {
    origin = first.match(/^https?:\/\/[^/]+/i)?.[0] ?? '';
    if (origin) {
      first = first.replace(origin, '');
      pathsOrParams[0] = first;
    }
  }

  const str = pathsOrParams.reduce((ret, item) => {
    let pathPart = item;
    let [before, after = ''] = ret.split('?');
    if (isObject(pathPart)) pathPart = `?${objectToParamStr(pathPart)}`;
    if (typeof pathPart !== 'string') return ret;

    if (['=', '&', '?'].some((v) => pathPart.indexOf(v) !== -1)) {
      let [partBefore, partAfter] = pathPart.split('?');
      if (partAfter == null) {
        partAfter = partBefore;
        partBefore = '';
      }
      if (partBefore) {
        before += `${before ? '/' : ''}${partBefore}`;
      }

      after = objectToParamStr({
        ...parseQueryString(after),
        ...parseQueryString(partAfter),
      });
    } else {
      before += `${before ? '/' : ''}${pathPart}`;
    }
    if (after) before += `?${after}`;
    return before;
  }, '');

  return (origin + str).replace(/\/{2,}/g, '/');
}

/**
 * @param {Object} a
 * @param {Object} b
 * @param {Object} [options]
 * @property {Boolean} [deep=false]        是否进行深度比对
 * @property {Boolean} [checkKey=false]    是否比对key是否存在
 * @property {Boolean} [strict=false]      是否严格比对
 * @property {Function} compare            对比函数
 * @return {boolean}
 */
export function isEqual(a, b, { deep = false, checkKey = false, strict = false, compare } = {}) {
  const _isEqual = (
    a,
    b,
    { deep = false, checkKey = false, strict = false, compare } = {},
    circles = { a: [], b: [] },
    key = [],
  ) => {
    const equalFn = compare || (strict ? (aa, bb) => aa === bb : (aa, bb) => aa == bb);
    const typeA = typeof a;
    const typeB = typeof b;
    if (typeA !== 'object' && typeB !== 'object') {
      return equalFn(a, b, key);
    } else if (equalFn(a, null, key) || equalFn(b, null, key)) {
      return equalFn(a, b, key);
    } else if (typeA !== typeB && strict) {
      return false;
    }

    // 防止循环结构
    if (circles.a.includes(a)) return true;
    circles.a.push(a);
    if (circles.a.includes(b)) return true;
    circles.b.push(b);

    const keysA = Object.keys(a);
    const keysB = Object.keys(b);
    const keys = arrayUnique([...keysA, ...keysB]);

    const nextKey = (next) => [...key, next];
    if (deep || checkKey) {
      for (let key of keys) {
        const $key = nextKey(key);
        if (checkKey) {
          if (!keysA.includes(key) || !keysB.includes(key)) return false;
        }
        if (deep) {
          const equal = _isEqual(a[key], b[key], { deep, checkKey, strict, compare }, circles, $key);
          if (!equal) return false;
        }
      }
      return true;
    } else {
      return equalFn(a, b, key);
    }
  };
  return _isEqual(a, b, { deep, checkKey, strict, compare });
}

export function isEqualDeep(a, b) {
  return isEqual(a, b, { deep: true });
}

export function isEqualStrictDeep(a, b) {
  return isEqual(a, b, { deep: true, checkKey: true, strict: true });
}

export function isEqualMap(a, b) {
  return isEqualStrictDeep(map(a), map(b));
}

/**
 * 将对象按指定的key拆成两个对象返回
 * 返回的第一个对象是指定的key的值，返回的第二个对象是非指定key的值
 * @param {Object} object
 * @param {String[]} keys
 * @return {[Object, Object]}
 */
export function pick(object, keys) {
  return keys.reduce(
    ([left, right], key) => {
      if (Object.hasOwnProperty.call(right, key)) {
        left[key] = right[key];
        delete right[key];
      }
      return [left, right];
    },
    [{}, { ...object }],
  );
}

export function isPromise(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

export async function wrapPromise(promise) {
  try {
    return [await promise, null];
  } catch (error) {
    return [null, error];
  }
}

export { wrapPromise as wp };

/**
 * 返回唯一列表
 * @param {any[]} array
 * @return {any[]}
 */
export function arrayUnique(array) {
  return Array.from(new Set(array));
}

/** 判断返回值 */
export async function checkReturn(res) {
  if (res == null) return true;
  if (typeof res === 'boolean') return res;
  if (isPromise(res)) {
    try {
      return checkReturn(await res);
    } catch (e) {
      console.error(e);
      return false;
    }
  }
  return Boolean(res);
}

export async function getReturn(res) {
  if (isPromise(res)) return await res;
  return res;
}

let handlerScope = null;

export function useForkHandlerScope(scope) {
  const prev = handlerScope;
  handlerScope = scope;
  return function popHandlerScope() {
    handlerScope = prev;
  };
}

/**
 * 合并多个回调函数
 * 最后一个参数如果是true, 会用以下方式检查函数返回结果
 * 1. 如果其中一个结果是false或者抛出异常，会阻断后面的函数
 * 2. 会等待异步回调执行结束后再调用下一个回调
 */
export function forkHandler(...handlers) {
  const [last] = handlers.slice(-1);
  let checkResult = false;
  if (typeof last === 'boolean') {
    checkResult = last;
    handlers.pop();
  }
  const catchFns = [];
  const fn = async (...args) => {
    for (const fn of handlers) {
      if (!(await checkReturn(handlerScope?.()))) break;
      let ret;
      try {
        ret = fn?.(...args);
      } catch (e) {
        console.error(e);
        if (checkResult) return false;
      }
      if (checkResult) {
        if (!(await checkReturn(ret))) {
          for (const fn of catchFns) {
            try {
              fn?.();
            } catch {}
          }
          return false;
        }
      }
    }
  };
  fn.catch = (catchFn) => {
    catchFns.push(catchFn);
    return fn;
  };
  return fn;
}

export { forkHandler as fh };

export function forkCall(mountedRef, fn, ...args) {
  // 如果用了forkHandler 这个scope会判断是否需要继续调用下一个事件
  const popScope = useForkHandlerScope(() => mountedRef.current);

  try {
    const ret = fn?.(...args);
    if (isPromise(ret)) {
      ret.finally(popScope);
    } else {
      popScope();
    }

    if (ret != null) return ret;
  } catch (e) {
    popScope();
    throw e;
  }
}

/** 生成唯一ID */
export function uid() {
  return Math.random().toString(36).substr(2) + Date.now().toString(36);
}

export function isEmpty(object) {
  return isObject(object) && !Object.keys(object).length;
}

export function isNull(value) {
  return new Set([null, undefined, NaN, '']).has(value);
}

export function notNull(value) {
  return !isNull(value);
}

export function isRef(object) {
  if (!isObject(object)) return false;
  return Reflect.ownKeys(object).join(',') === 'current';
}

export function map(object, fn, scope = object) {
  if (object == null) return null;
  const ret = [];
  const itr = object[Symbol.iterator] ? object.entries() : isObject(object) ? Object.entries(object) : null;
  if (!itr) throw new Error('object is not iterable.');

  for (const [key, value] of itr) {
    ret.push(fn?.call(scope, value, key, object) ?? [key, value]);
  }

  return ret;
}

export const defaultShouldUpdate = invert(isEqualStrictDeep);

export function getType(object) {
  const type = Object.prototype.toString.call(object);
  return type.substr(8, type.length - 9);
}

export function mergeState(prevState, newState) {
  const typePrev = getType(prevState);
  const typeNew = getType(newState);
  if (typePrev !== typeNew) return newState;
  switch (typeNew) {
    case 'Array':
      return newState.map((newItem, index) => {
        return mergeState(prevState[index], newItem);
      });
    case 'Object':
      return Object.keys(newState).reduce(
        (ret, key) => ((ret[key] = mergeState(ret[key], newState[key])), ret),
        Object.assign({}, prevState),
      );
    default:
      return newState;
  }
}

export function simpleMerge(...objectList) {
  return objectList.reduce(mergeState);
}

export function getToken() {
  return sessionStorage.getItem('token');
}

export function setToken(token) {
  return sessionStorage.setItem('token', token);
}

export function flattenTree(treeList, options) {
  const flatMap = (list, opts, depth = 0, parent) => {
    const { childKey = 'children', includeParent = true, map = (v) => v } = opts || {};

    return list.flatMap(({ [childKey]: children, ...item }) => {
      const retItem = map(item, depth, parent);
      if (children?.length) {
        return [...(includeParent ? [retItem] : []), ...flatMap(children, opts, depth + 1, retItem)];
      }
      return [retItem];
    });
  };
  return flatMap(treeList, options);
}

export function composeTree(flatList, { parentKey = 'parentId', childKey = 'children', key = 'id', rootId = 0 } = {}) {
  const groupByParent = {};
  flatList.forEach((item) => {
    const parentId = item[parentKey] ?? rootId;
    const selfId = item[key];
    groupByParent[parentId] = groupByParent[parentId] || [];
    groupByParent[selfId] = groupByParent[selfId] || [];
    const itemWithChildren = { ...item, [childKey]: groupByParent[selfId] };
    groupByParent[parentId].push(itemWithChildren);
  });

  return groupByParent[rootId];
}

export function groupBy(collection, byKeys) {
  return collection.reduce((group, item) => {
    const key = get(byKeys)(item);
    if (!group[key]) group[key] = [];
    group[key].push(item);
    return group;
  }, {});
}

export function JSONParseFallback(json) {
  try {
    return JSON.parse(json);
  } catch (e) {
    return json;
  }
}

export function initialValueByQuery(queryKey) {
  return {
    getItemProps() {
      return { initialValue: useQueryValue(queryKey) };
    },
  };
}

export const commonRules = {
  code: [
    {
      pattern: /^[a-z][0-9a-z-_]*$/i,
      message: '只允许输入大小写字母、数字、下划线组合，第一位必须是字母',
    },
  ],
  dictCode: [
    {
      pattern: /^\w+$/i,
      message: '只允许输入字母,数字,-和_ 组合',
    },
  ],
};

export const { getAccept, isImageExt, isVideoExt } = ((fileTypes) => ({
  getAccept(...types) {
    if (types.length === 1 && types[0] === 'all') {
      types = ['image', 'doc', 'video', 'excel'];
    }
    return types.reduce((ret, name) => ret.concat(fileTypes[name]), []).join(',');
  },
  isImageExt(fileExt) {
    return fileTypes.image.some((v) => v === `.${fileExt}`.toLowerCase());
  },
  isVideoExt(fileExt) {
    return fileTypes.video.some((v) => v === `.${fileExt}`.toLowerCase());
  },
}))({
  image: ['.jpg', '.png', '.gif', '.webp', '.svg'],
  pdf: ['.pdf'],
  doc: ['.pdf', '.doc', '.docx'],
  excel: ['.xls', '.xlsx', '.csv'],
  video: ['.mp4', '.mov', '.flv', '.webm', '.avi'],
  audio: ['.mp3', '.acc', '.wav'],
});

export function joinComma(props = null) {
  return {
    ...props,
    getValueFromEvent(val) {
      val = val?.filter(notNull);
      if (!val?.length) return null;
      return val.join(',');
    },
    getValueProps(value) {
      if (Array.isArray(value)) {
        return { value };
      }
      return { value: value?.split(',').filter(notNull) || [] };
    },
  };
}

export function boolean(props = null, { true: a = '1', false: b = '0', prop = 'checked' } = {}) {
  return {
    initialValue: b,
    ...props,
    getValueFromEvent(event) {
      let value = [event?.target?.checked, ...arguments].find((v) => typeof v === 'boolean');
      return value === true ? a : b;
    },
    getValueProps(value) {
      const propValue = value === a;
      return { [prop]: propValue };
    },
  };
}

export function filterNullKey(object, $isNull = (v) => v == null) {
  const newObject = { ...object };
  for (const key in newObject) {
    if ($isNull(newObject[key])) delete newObject[key];
  }
  return newObject;
}

export function modalWith(max, min = 300, gap = 30) {
  const size = (v) => (`${v}` === `${Number(v)}` ? `${v}px` : v);

  return `max(${size(min)}, min(${size(max)}, calc(100vw - ${size(gap)})))`;
}

export function off(el, type, fn, attr) {
  el.removeEventListener(type, fn, attr);
}

export function on(el, type, fn, attr) {
  el.addEventListener(type, fn, attr);
  return () => {
    off(el, type, fn, attr);
  };
}

export function once(el, type, fn, attr) {
  let $off;
  const $fn = function () {
    fn.apply(this, arguments);
    $off();
    $off = () => {};
  };
  $off = el.addEventListener(type, $fn, attr);
}

const _raf = window.requestAnimationFrame;

let nextFrameCallbacks = [];
let nextFrameRun = false;

export function nextFrame(fn) {
  nextFrameCallbacks.unshift(fn);
  if (nextFrameRun) return;
  nextFrameRun = true;
  _raf(() => {
    nextFrameRun = false;
    const cbs = nextFrameCallbacks;
    nextFrameCallbacks = [];
    for (let i = cbs.length - 1; i > -1; i--) cbs[i]();
  });
}

export function onFrame(fn, immediate) {
  let close = 0;

  function $runFrame() {
    if (close === true) return;
    if (immediate || close === false) fn();
    close = false;
    _raf($runFrame);
  }

  $runFrame();

  return () => {
    close = true;
  };
}
